package core1;

import java.util.LinkedHashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import java.util.Collections;

import org.gicentre.utils.move.ZoomPan;

import processing.core.PApplet;
import processing.core.PVector;

import fullscreen.*;


public  class Main extends PApplet {

	
	
	Globals globals = Globals.createInstance(this);
	private static final long serialVersionUID = 1L;
	private float scale = 1; 

	public  List<Nodeable> nodes;
	public  List<StandAloneChordNode> sanodes;
	
	private Map<Long, MirrorChordNode> minodes;
	
	private List<ChordElement> elements;
	protected List<ChordObject> objects; //For now, made this public within the package so that ChordNode can use it for the mesh mode 

	private final static JavaNode node = null; // new JavaNode();  // skip for the presentation.  avid errors and gain performance.
	private FullScreen fs;
	
	// MAIN
	public static void main(String args[]) {
		
		String[] pargs = new String[100];
		int index = 0;
		
		for (String arg:args) {
			if (arg.equals("--fullscreen")) {
				System.out.println("Fullscreen mode enabled");
				pargs[index] = "--present";
				index++;
			}
		}
		pargs[index] = "core1.Main";
		index++;
		
		PApplet.main(pargs);
	}

	public Main() {
		
		Globals.getInstance().setMain(this);
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				if(node !=null) 
				node.start();
				
			}
		}).start();
	}
	
	// Processing Setup()
	// This is called once by processing
	public void setup() {
		globals = Globals.createInstance(this);
		Globals.getInstance().setParent(this);
		globals.zoomer = new ZoomPan(this);
		fs = new FullScreen(this);
		
		nodes = Collections.synchronizedList(new ArrayList<Nodeable>());
		sanodes = Collections.synchronizedList(new ArrayList<StandAloneChordNode>());
		minodes = Collections.synchronizedMap(new LinkedHashMap<Long, MirrorChordNode>());
		objects = Collections.synchronizedList(new ArrayList<ChordObject>());
		elements = Collections.synchronizedList(new ArrayList<ChordElement>());
		
		hint(ENABLE_STROKE_PURE);
		updateFullScreen();
		
		background(globals.BGColor);
		//zoomer.setMinZoomScale(.10);
		//zoomer.setMaxZoomScale(10);
		globals.zoomer.setZoomScale(0.2);
		//globals.zoomer.setPanOffset(globals.displayWidth  /2, globals.displayHeight/2);
		globals.zoomer.setZoomMouseButton(RIGHT);
		
		
		setLogicView();

	}
	
	


	//Processing Draw()
	// This is called every frame by Processing.
	public void draw() {
		
		//globals.alreadyDrawn = new ArrayList<Pair<Nodeable,Nodeable>>();
		
		background(globals.BGColor);

		globals.zoomer.transform();
		globals.scale = (float) globals.zoomer.getZoomScale();
		noFill();
		strokeWeight(2/globals.scale);
		stroke(globals.setOpacity(globals.RingColor,globals.ringOpacity));
		ellipse(0,0,(2*Globals.radius),(2*Globals.radius));
		synchronized(objects) {
			
			double selectedDist = 0;
			ChordNode selectedNode = null;
			
			// Find the node closest to the mouse pointer
			for (ChordObject chordObject :objects) {
				if (chordObject instanceof ChordNode) {
					ChordNode chordNode = (ChordNode) chordObject;
					double dist = chordNode.mouseDistanceScore();
					if (dist < selectedDist || selectedDist == 0) {
						selectedDist = dist;
						selectedNode = chordNode;
					}
				}
			}
			
			// draw nodes, first pass
			for (Nodeable node :nodes) {
				pushMatrix();
				pushStyle();
				node.draw(globals.scale);
				popStyle();
				popMatrix();
			}
			
			// draw nodes , second pass ( draw nodes on top of fingers)
			for (Nodeable node :nodes) {
				pushMatrix();
				pushStyle();
				node.draw2(globals.scale);
				popStyle();
				popMatrix();
			}
			
			
			// elements on top of everything 
			for (ChordElement e :elements) {
				pushMatrix();
				pushStyle();
				e.draw(globals.scale);
				popStyle();
				popMatrix();
				
			}
			
			if (globals.targetNode != null)
				globals.targetNode.drawTargetNodeHint(globals.scale);
			
			pushMatrix();
			pushStyle();
			if (selectedNode != null && selectedNode.mouseOver())
				selectedNode.onMouseOver(globals.scale);
			popStyle();
			popMatrix();
		}
		
		PVector mouse = globals.zoomer.getMouseCoord();
		resetMatrix();
		drawLegend();
		drawStatus();
		drawBigTitle();
		//text(mouse.x+" , "+mouse.y, mouseX, mouseY);

	}
	
	private void drawBigTitle(){
		
		String networkDesc ="";
		switch(globals.networkMode) {
		case Globals.CHORD_MODE: networkDesc = "Chord"; break;
		case Globals.MESH_MODE: networkDesc = "FCN"; break;
		case Globals.SIMPLE_MODE: networkDesc = ""; break;
	}
		
		pushMatrix();
		resetMatrix();
		pushStyle();
		textSize(50);
		fill(0,0,0,128);
		text(networkDesc,globals.displayWidth-200f,globals.displayHeight-50f);
		popStyle();
		popMatrix();
		
	}
	
	

	// add a new standalone Chord Node
	public void addStandAloneNode(){
		StandAloneChordNode cn = new StandAloneChordNode(this);

		cn.setLogicView();
		if (sanodes.size() > 0)
			cn.addMeToYourChord(sanodes.get(0));
		sanodes.add(cn);
		nodes.add(cn);
		objects.add(cn); 
		if (sanodes.size() == 1)
			globals.targetNode = cn;
		updateView();
	}
	
	public void addStandAloneNode(long key) {
		
		StandAloneChordNode cn = new StandAloneChordNode(this, key);

		cn.setLogicView();
		if (sanodes.size() > 0)
			cn.addMeToYourChord(sanodes.get(0));
		sanodes.add(cn);
		nodes.add(cn);
		objects.add(cn); 
		if (sanodes.size() == 1)
			globals.targetNode = cn;
		updateView();
	}

	public void addUpdateMirrorNode(long key, String name)
	{
		MirrorChordNode found = minodes.get(key);
		if (found == null) {
			addMirrorNode(key, name);
		} else {
			updateMirrorNode(found, key, name);
		}
	}
	private void addMirrorNode(long key, String name) {
		MirrorChordNode cn = new MirrorChordNode(this, key, name);
		
		cn.setLogicView();
		minodes.put(key, cn);
		nodes.add(cn);
		objects.add(cn);
		updateView();
	}
	private void updateMirrorNode(MirrorChordNode node, long key, String name) {
		node.setLogicView();
		updateView();
	}

	public void mousePressed() {
		
		if( onRing() ) {
			
			addStandAloneNode(getKeyOnMouseCoords());
		}
		
	}
	
	private long getKeyOnMouseCoords() {
		
		PVector mousePos = Globals.getInstance().zoomer.getMouseCoord();
		
		double angle = ((Math.atan2(mousePos.y, mousePos.x) * 180/Math.PI));
		double adjustedAngle = ((angle+720)%360);
		
		return Math.round( (adjustedAngle/360) * Globals.getInstance().maxKey );
	}
	
	private boolean onRing() {
		
		PVector mousePos = Globals.getInstance().zoomer.getMouseCoord();
		
		float treshold = 20.0f;
		
		float pyth = (float) Math.sqrt( Math.pow(mousePos.x, 2) + Math.pow(mousePos.y, 2) );
		
		return pyth > Globals.radius - (treshold/globals.scale)
			&& pyth < Globals.radius + (treshold/globals.scale);
	}
	
	private int toBin(boolean x) {
		if (x)
			return 1;
		else
			return 0;
	}
	
	public void drawLegend() {
		
		fill(globals.TextColor);
		
		pushMatrix();
		resetMatrix();
		int legendX = 10;
		int legendY = 10;
		
		if (globals.drawLegend) 
		{
			text(
				 "Space  -Add Element \n" +
				 "N      -Add Node \n" +
				 "F      -Fullscreen \n" +
				 "R      -Refresh Elements- \n" +
				 "U      -Force Update Fingers \n" +
				 "1      -Logical View \n" +
				 "2      -Physical View \n" +
				 "L     " + toBin(globals.drawLegend) + " -Toggle Legend \n" +
				 "B     " + toBin(globals.drawFullFingerTables) + " -Toggle Finger tables \n" +
				 "T     " + toBin(globals.drawText) + " -Toggle Text \n" +
				 "Y     " + toBin(globals.drawAlgo) + " -Toggle Query paths \n" +
				 "V     " + globals.networkMode + " -Toggle Chord/FCN/Simple \n" +
				 "P     " + toBin(globals.drawPreds) + " -Toggle Predecessor pointers \n" +
				 "M      -Change Color Theme \n" +
				 "C      -Clear \n" +
				 "Q     " + globals.querySpeed + " -Toggle Network Speed \n" +
				 "X      -Toggle ring opacity \n" +
				 "ESC     -Quit\n" 
				 
			, legendX, legendY);
			
		};
		popMatrix();
	}
	public void drawStatus() {
		pushStyle();
		fill(globals.TextColor);
		textSize(globals.statusSize);
		pushMatrix();
		
		resetMatrix();
		int statusX = 750;
		int statusY = 20;
		
		int sanodecount = sanodes.size();
		int minodecount = minodes.size();
		int allnodecount = nodes.size();
		int connectioncount = 0;
		int elemcount = 0;
		
		String networkDesc = null;
		String operationModeDesc = null;
		String viewModeDesc = null;
		
		switch(globals.networkMode) {
			case Globals.CHORD_MODE: networkDesc = "Chord"; break;
			case Globals.MESH_MODE: networkDesc = "FCN"; break;
			case Globals.SIMPLE_MODE: networkDesc = "Simple"; break;
		}
		
		if (sanodecount > 0 && minodecount > 0) {
			operationModeDesc = "Mixed mode";
		} else if (sanodecount > 0) {
			operationModeDesc = "Simulation";
		} else if (minodecount > 0) {
			operationModeDesc = "Absolute";
		} else {
			operationModeDesc = "Simulation";
		}
		
		if (globals.logicView) {
			viewModeDesc = "Logical";
		} else {
			viewModeDesc = "Physical";
		}
		
		if (globals.networkMode == Globals.SIMPLE_MODE) {
			connectioncount = sanodecount * 2;
		} else if (globals.networkMode == Globals.MESH_MODE) {
			connectioncount = sanodecount * (sanodecount - 1);
		} else if (globals.networkMode == Globals.CHORD_MODE) {
			connectioncount = sanodecount * globals.bigM;
		}
		
		for (Nodeable node:nodes) {
			elemcount += node.getElementCount();
		}
		text(
				"Algorithm:                    " + "\n" +
				"Mode:                  " + "\n" +
				"View:                       " + "\n" +
				"Hash Size:              " + "\n" +
				"Key ring size:                   " + "\n" +
				"Nodes:     " + "\n" +
				//"Number of mirrored nodes:        " + "\n" +
				"Elements:        " + "\n" +
				"Connections:     " + "\n" +
				
				"", statusX, statusY);
		
		text(
				networkDesc + "\n" +
				operationModeDesc + "\n" +
				viewModeDesc + "\n" +
				globals.bigM + " bit\n" +
				globals.maxKey + "\n" +
				sanodecount + "\n" +
			//	minodecount + "\n" +
				elemcount + "\n" +
				connectioncount + "\n" +
				
				"", statusX+150, statusY);
		
		popMatrix();
		popStyle();
	}
	
	public void keyPressed() {
		switch (key) {

		case 'c':
			setup();
			break;
		case ' ':
		case 'e':
			addStandAloneElement();
			break;
		case 'n':
			addStandAloneNode();
			break;
		case 'u':
			for(StandAloneChordNode node:sanodes)
			{
				node.updateFingerTable();
			}
			break;
		case 'f':
			globals.fullscreen = !globals.fullscreen;
			updateFullScreen();
			break;
		case 'q':
			// First perform modular, possibly down to zero, then increment. 
			globals.querySpeed = (globals.querySpeed + globals.querySpeedChangeIncrement) % globals.queryMaxSpeed ;
			break;
		case 'l':
			globals.drawLegend = !globals.drawLegend;
			break;
		case 'r':
			refreshAllElements();
			break;
		case '1':
			setLogicView();
			break;
		case '2':
			setPhysicalView();
			break;
		case 't':
			globals.drawText = !globals.drawText;
			break;
		case 'b':
			globals.drawFullFingerTables = !globals.drawFullFingerTables;
			break;
		case 'm':
			globals.ColorThemeIndex = (globals.ColorThemeIndex + 1) % globals.ColorThemes.length;
			
			globals.setupColors();
			break;
		case 'v':
			globals.networkMode = (globals.networkMode + 1) % Globals.NETWORK_MODE_COUNT;
			break;
		case 'y':
			globals.drawAlgo = !globals.drawAlgo;
			break;
		case 'p':
			globals.drawPreds = !globals.drawPreds;
			break;
		case 'x':
			globals.ringOpacity = (globals.ringOpacity + 64) %255;
			break;
		default:
			
		}
	}

// Set the "physical" view, where all elements are shown 
//	where they are stored. 
	private void setPhysicalView() {

		globals.logicView=false;
		for(Nodeable chordNode:nodes)
		{
			chordNode.setPhysicalView();
		}
	}

	// Set the view to "logic" where each object
	// is displayed on its logical position on the chord ring.

	public void setLogicView() {
		globals.logicView=true;
		for (ChordObject chordObject :objects){
			chordObject.setLogicView();
		}

		// keyPressed() 
		// This method handles keyboard input and is called whenever a key is pressed.

	}

	private void addStandAloneElement() {
		if (sanodes.size() >0){	
			ChordElement e = new ChordElement(this);
			ChordQuery q = null;
			StandAloneChordNode selectedNode = null;
			
			elements.add(e);
			objects.add(e);
			if (!sanodes.isEmpty())
				sanodes.get(0).addElement(e);
			
			if (globals.drawAlgo) {
				selectedNode = (StandAloneChordNode) sanodes.get(0).find(e.getKey());
				q = new ChordQuery(this, globals.targetNode,selectedNode,e.getKey());
				
				e.setQuery(q);
			}
			updateView();
		}

	}
	
	/*public void addStandAloneElement(long hash) {
		ChordElement e = new ChordElement(this, hash);

		elements.add(e);
		objects.add(e);
		if (!sanodes.isEmpty())
			sanodes.get(0).addElement(e);
		if (globals.drawAlgo)
			globals.testquery = new ChordQuery(this, globals.targetNode,(StandAloneChordNode) sanodes.get(0).find(e.getKey()));
		
		updateView();
	}
	*/
	public void addMirrorElement(long hash, long node, String key, String value) 
	throws Exception {
		ChordElement e = new ChordElement(this, hash, key, value);
		
		MirrorChordNode minode = findMirrorChordNode(node);
		if (minode == null)
			throw new Exception("Could not find target node for element");
		
		minode.addElement(e);
		
		elements.add(e);
		objects.add(e);
		updateView();
	}
	public void updateView()
	{
		if (globals.logicView)
			setLogicView();
		else
			setPhysicalView();
	}
	public void updateFullScreen()
	{
		// The following is for a setup with dual screens and a large second display
		// Note that it overloads variables that are already in scope. 
		// 
		// Before the final presentation, this is set to the projector in the
		// lecture hall's resolution. 
		int displayWidth = globals.displayWidth;
 		int displayHeight = globals.displayHeight;
		if (globals.fullscreen) {
			fs.enter();
			size(displayWidth, displayHeight);
		} else {
			fs.leave();
			size(displayWidth,displayHeight);
		}
	}
	public void refreshAllElements(){
		for(Nodeable node:nodes)
		{
			node.refreshElements();
		}
		setPhysicalView();
		
	}
	
	public void addUpdateMirrorNode(long key, String name, long predKey, List<Long> fingerKeys)
	throws Exception {
		MirrorChordNode found = minodes.get(key);
		if (found == null) {
			addMirrorNode(key, name, predKey, fingerKeys);
		} else {
			updateMirrorNode(found, key, name, predKey, fingerKeys);
		}
	}
	private void addMirrorNode(long key, String name, long predKey, List<Long> fingerKeys)
	throws Exception {
		System.out.println("VISUAL: Adding node " + name + " at pos " + key);
		
		MirrorChordNode predNode = null;
		List<MirrorChordNode> fingerNodes = null;
		
		if (predKey != key) {
			predNode = minodes.get(predKey);
			
			if (predNode == null)
				throw new Exception("Couldn't find predecessor node at pos " + predKey
						    + " while adding node at pos " + key);
		}
		if (fingerKeys != null) {
			fingerNodes = new ArrayList<MirrorChordNode>();
			
			for (Long fingerKey:fingerKeys) {
				MirrorChordNode fingerNode = minodes.get(fingerKey);
				if (fingerNode == null)
					throw new Exception("Couldn't find finger node at pos " + fingerKey
							+ " while adding node at pos " + key);
				fingerNodes.add(fingerNode);
			}
		}
		MirrorChordNode cn = new MirrorChordNode(this, key, name, predNode, fingerNodes);
		
		cn.setLogicView();
		minodes.put(key, cn);
		nodes.add(cn);
		objects.add(cn);
		updateView();
	}
	private void updateMirrorNode(MirrorChordNode node, long key, String name, long predKey, List<Long> fingerKeys)
	throws Exception {
		System.out.println("VISUAL: Updating node " + name + " at pos " + key);
		
		MirrorChordNode predNode = null;
		List<MirrorChordNode> fingerNodes = null;
		
		if (predKey != key) {
			predNode = minodes.get(predKey);
			
			if (predNode == null)
				throw new Exception("Couldn't find predecessor node at pos " + predKey
						    + " while adding node at pos " + key);
		}
		if (fingerKeys != null) {
			fingerNodes = new ArrayList<MirrorChordNode>();
			
			for (Long fingerKey:fingerKeys) {
				MirrorChordNode fingerNode = minodes.get(fingerKey);
				if (fingerNode == null)
					throw new Exception("Couldn't find finger node at pos " + fingerKey
							+ " while adding node at pos " + key);
				fingerNodes.add(fingerNode);
			}
		}
		node.updateNode(key, name, predNode, fingerNodes);
		
		node.setLogicView();
		updateView();
	}
	public MirrorChordNode findMirrorChordNode(long key) {
		return minodes.get(key);
	}

}
